package com.wei.czz.framework.common.wrapper;

import com.wei.czz.common.constant.Constant;
import com.wei.czz.common.constant.EntityConstant;
import com.wei.czz.common.constant.RedisConstant;
import com.wei.czz.common.enums.CommonEnum;
import com.wei.czz.common.enums.ResultEnum;
import com.wei.czz.common.exception.CzzException;
import com.wei.czz.common.property.MinioProperty;
import com.wei.czz.common.utils.FileUtils;
import com.wei.czz.common.utils.RandomNumUtils;
import com.wei.czz.common.utils.ShaUtils;
import com.wei.czz.framework.common.entity.FileChunkFlowEntity;
import com.wei.czz.framework.common.entity.FileFlowEntity;
import com.wei.czz.framework.common.handler.ThrottleHandler;
import com.wei.czz.framework.common.handler.redis.RedisHandler;
import com.wei.czz.framework.common.handler.redis.RedisStringHandler;
import com.wei.czz.framework.common.service.*;
import io.minio.*;
import io.minio.http.Method;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.tika.Tika;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.Pipe;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Created by IntelliJ IDEA.
 *
 * @author wyw
 * date: 2025-03-22 14:17:27
 * className: MinioWrapper Minio操作包装类
 * version: 1.1
 * description:
 */
@Component
@AllArgsConstructor
public class MinioWrapper {

    private static final Logger log = LoggerFactory.getLogger(MinioWrapper.class);

    private static final DateFormat PATH_DATE_FORMAT = new SimpleDateFormat("yyyy-MM/dd");

    private static final String MINIO = "minio";

    private static final String CHUNK_FILE = "chunkFile";

    private final MinioProperty minioProperty;

    private final FileFlowService fileFlowService;

    private final FileChunkFlowService fileChunkFlowService;

    private final AsyncService asyncService;

    private final TransactionService transactionService;

    private final SnowflakeService snowflakeService;

    private final MinioClient minioClient;

    private final RedisHandler redisHandler;

    private final RedisStringHandler redisStringHandler;

    private final RedissonClient redissonClient;

    private final ThrottleHandler throttleHandler;

    /**
     * 上传文件
     * @param file 文件
     */
    public Long uploadFile(MultipartFile file) {

        // 获取文件的哈希值
        String fileHash = ShaUtils.encodeSha256(file);

        /*
            上传文件
         */
        return this.uploadFile(fileHash, file);
    }

    /**
     * 上传文件
     * @param fileHash 文件哈希值
     * @param file     文件
     */
    public Long uploadFile(String fileHash, MultipartFile file) {
        // 文件名称
        String fileName = file.getOriginalFilename();
        if (StringUtils.isBlank(fileName)) {
            log.error("文件名称为空。fileHash={}", fileHash);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "文件名称不能为空");
        }

        /*
            获取文件上传记录
         */
        FileFlowEntity fileFlow = this.getFileFlow(fileHash);
        // 检查文件是否已存在
        if (Objects.nonNull(fileFlow)) {
            log.info("文件已存在，无需重复上传。fileHash={}", fileHash);
            /*
                复制文件上传记录
             */
            Long fileNo = fileFlow.getFileNo();
            if (!fileName.equals(fileFlow.getOriginalFileName())) {
                // 文件名称不一致，复制文件上传记录
                fileNo = fileFlowService.copy(fileFlow.getId(), file.getOriginalFilename());
            }
            return fileNo;
        }

        // 生成文件路径
        String path = PATH_DATE_FORMAT.format(System.currentTimeMillis());

        /*
            上传文件
         */
        this.upload(file, minioProperty.getBucketName(), path, fileName);

        // 获取文件后缀
        String fileSuffix = fileName.substring(fileName.lastIndexOf(".") + 1);

        /*
            保存文件上传记录
         */
        FileFlowEntity saveFileFlow = new FileFlowEntity();
        saveFileFlow.setFileHash(fileHash)
                .setFilePath(path)
                .setFileName(fileName)
                .setOriginalFileName(fileName)
                .setFileSize(file.getSize())
                .setFileType(file.getContentType())
                .setFileSuffix(fileSuffix)
                .setBucketName(minioProperty.getBucketName())
                .setBigFileFlag(CommonEnum.ONE.getValue())
                .setChunkNum(CommonEnum.MINUS_ONE.getValue())
                .setChunkSize(CommonEnum.MINUS_ONE.getLongValue());
        fileFlowService.saveOrEdit(saveFileFlow);

        return saveFileFlow.getFileNo();
    }

    /**
     * 上传文件
     * @param file 文件
     * @return 文件编号
     */
    public Long uploadFile(File file) {
        if (!file.exists()) {
            log.error("文件不存在。file={}", file.getAbsolutePath());
            throw new CzzException();
        }

        // 文件名称
        String fileName = file.getName();

        // 获取文件的哈希值
        String fileHash = ShaUtils.fileSha256(file);

        /*
            获取文件上传记录
         */
        FileFlowEntity fileFlow = this.getFileFlow(fileHash);
        // 检查文件是否已存在
        if (Objects.nonNull(fileFlow)) {
            log.info("文件已存在，无需重复上传。fileHash={}", fileHash);
            /*
                复制文件上传记录
             */
            Long fileNo = fileFlow.getFileNo();
            if (!fileName.equals(fileFlow.getOriginalFileName())) {
                // 文件名称不一致，复制文件上传记录
                fileNo = fileFlowService.copy(fileFlow.getId(), file.getName());
            }
            return fileNo;
        }

        // 生成文件路径
        String path = PATH_DATE_FORMAT.format(System.currentTimeMillis());

        // 获取文件类型
        String contentType;
        try {
            contentType = Optional.ofNullable(new Tika().detect(file)).orElse(StringUtils.EMPTY);
        } catch (IOException e) {
            log.error("获取文件类型异常。message={}", e.getMessage(), e);
            throw new CzzException();
        }

        /*
            上传文件
         */
        this.upload(file, minioProperty.getBucketName(), path, fileName, contentType);

        // 获取文件后缀
        String fileSuffix = fileName.substring(fileName.lastIndexOf(".") + 1);

        /*
            保存文件上传记录
         */
        FileFlowEntity saveFileFlow = new FileFlowEntity();
        saveFileFlow.setFileHash(fileHash)
                .setFilePath(path)
                .setFileName(fileName)
                .setOriginalFileName(fileName)
                .setFileSize(file.length())
                .setFileType(contentType)
                .setFileSuffix(fileSuffix)
                .setBucketName(minioProperty.getBucketName())
                .setBigFileFlag(CommonEnum.ONE.getValue())
                .setChunkNum(CommonEnum.MINUS_ONE.getValue())
                .setChunkSize(CommonEnum.MINUS_ONE.getLongValue());
        fileFlowService.saveOrEdit(saveFileFlow);

        return saveFileFlow.getFileNo();
    }

    /**
     * 上传文件
     * @param inputStream 输入流
     * @param fileName    文件名称
     * @param contentType 文件类型
     * @return 文件编号
     */
    public Long uploadFile(InputStream inputStream, String fileName, String contentType) {
        // 生成文件路径
        String path = PATH_DATE_FORMAT.format(System.currentTimeMillis());

        // 文件存储名称
        String storageFileName = snowflakeService.find() + fileName;

        /*
            上传文件
         */
        this.upload(inputStream, minioProperty.getBucketName(), path, storageFileName, -1, contentType);

        /*
            获取文件元数据
         */
        StatObjectResponse statObjectResponse = this.getStat(minioProperty.getBucketName(), path, storageFileName);
        long size = statObjectResponse.size();
        log.info("文件大小：{}", size);

        // 获取文件后缀
        String fileSuffix = fileName.substring(fileName.lastIndexOf(".") + 1);

        /*
            保存文件上传记录
         */
        FileFlowEntity saveFileFlow = new FileFlowEntity();
        saveFileFlow.setFileHash(StringUtils.EMPTY)
                .setFilePath(path)
                .setFileName(storageFileName)
                .setOriginalFileName(fileName)
                .setFileSize(size)
                .setFileType(contentType)
                .setFileSuffix(fileSuffix)
                .setBucketName(minioProperty.getBucketName())
                .setBigFileFlag(CommonEnum.ONE.getValue())
                .setChunkNum(CommonEnum.MINUS_ONE.getValue())
                .setChunkSize(CommonEnum.MINUS_ONE.getLongValue());
        fileFlowService.saveOrEdit(saveFileFlow);

        /*
            如果不能直接获取到fileHash值，则考虑使用异步逻辑，下载文件并计算文件hash值
         */
        asyncService.execute(() -> {
            /*
                下载文件
             */
            try(InputStream _inputStream = this.getObject(minioProperty.getBucketName(), path, storageFileName)) {
                // 获取文件的哈希值
                String fileHash = ShaUtils.fileSha256(_inputStream, size);

                /*
                    更新文件上传记录
                 */
                FileFlowEntity updateFileFlow = new FileFlowEntity();
                updateFileFlow.setId(saveFileFlow.getId())
                        .setFileHash(fileHash);
                fileFlowService.saveOrEdit(updateFileFlow);
            } catch (IOException e) {
                log.error("文件流操作IO异常。message={}", e.getMessage(), e);
            }
        });

        return saveFileFlow.getFileNo();
    }

    /**
     * 上传文件
     * @param consumer    向输出流写入数据消费者对象
     * @param fileName    文件名称
     * @param contentType 文件类型
     * @return 文件编号
     */
    public Long uploadFile(Consumer<OutputStream> consumer, String fileName, String contentType) {
        /*
            采用管道流方式，直接将数据转成文件流写入minio，减少磁盘IO
         */
        // 创建 NIO 管道
        Pipe pipe;
        try {
            pipe = Pipe.open();
        } catch (IOException e) {
            log.error("创建nio管道对象异常。message={}", e.getMessage(), e);
            throw new CzzException();
        }

        try(// 获取管道输入通道
            Pipe.SinkChannel sinkChannel = pipe.sink();
            // 获取管道输出通道
            Pipe.SourceChannel sourceChannel = pipe.source();
        ) {

            // 将 SinkChannel 转换为 OutputStream
            OutputStream outputStream = Channels.newOutputStream(sinkChannel);

            // 将 SourceChannel 转换为 InputStream
            InputStream inputStream = Channels.newInputStream(sourceChannel);

            CompletableFuture<Long> completableFuture1 = asyncService.supplyAsync(() -> {
                try {
                    // 上传文件
                    return this.uploadFile(inputStream, fileName, contentType);
                } finally {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        log.error("关闭输入流异常。message={}", e.getMessage(), e);
                    }
                }
            });

            CompletableFuture<Void> completableFuture = asyncService.runAsync(() -> {
                try {
                    // 写数据
                    consumer.accept(outputStream);

                } finally {
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        log.error("关闭输出流异常。message={}", e.getMessage(), e);
                    }
                }
            });

            // 等待所有任务完成
            CompletableFuture.allOf(completableFuture, completableFuture1).get();

            // 返回文件编号
            return completableFuture1.get();

        } catch (IOException e) {
            log.error("管道io操作异常。message={}", e.getMessage(), e);
            throw new CzzException();
        } catch (ExecutionException | InterruptedException e) {
            log.error("文件流写入minio文件执行异常。message={}", e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 上传切片文件
     * @param fileHash      文件哈希值
     * @param chunkFileHash 切片文件哈希值
     * @param chunkSerial   切片序列号
     * @param file          切片文件
     */
    public void uploadChunkFile(String fileHash, String chunkFileHash, Integer chunkSerial, MultipartFile file) {
        /*
            获取切片文件上传记录
         */
        FileChunkFlowEntity fileChunkFlow = fileChunkFlowService.find(fileHash, chunkFileHash);
        // 检查文件是否已存在
        if (Objects.nonNull(fileChunkFlow)) {
            log.info("切片文件已存在，无需重复上传。fileHash={}, chunkFileHash={}", fileHash, chunkFileHash);
            return;
        }

        // 生成文件路径
        String path = CHUNK_FILE + "/" + fileHash;
        // 文件名称
        String fileName = chunkFileHash;

        /*
            上传文件
         */
        this.upload(file, minioProperty.getBucketName(), path, fileName);

        /*
            保存切片文件上传记录
         */
        FileChunkFlowEntity saveFileChunkFlow = new FileChunkFlowEntity();
        saveFileChunkFlow.setFileHash(fileHash)
                .setFilePath(path)
                .setFileName(fileName)
                .setFileSize(file.getSize())
                .setFileSuffix(StringUtils.EMPTY)
                .setChunkHash(chunkFileHash)
                .setChunkSerial(chunkSerial);
        fileChunkFlowService.saveOrEdit(saveFileChunkFlow);

    }

    /**
     * 文件压缩合并
     * @param fileNoList 文件编号列表
     * @param zipFileName 压缩文件名称
     * @return 压缩文件编号
     */
    public Long zipFile(List<Long> fileNoList, String zipFileName) {
        /*

         */
        MinioProperty.Zip zip = minioProperty.getZip();
        if (fileNoList.size() > zip.getMaxSize()) {
            log.info("单次文件压缩合并，文件数量超过限制阈值。list.size={} maxSize={}", fileNoList.size(), zip.getMaxSize());
            String message = "单次文件压缩合并，文件数量不能超过" + zip.getMaxSize() + "个";
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), message);
        }

        /*
            获取文件上传记录
         */
        List<FileFlowEntity> fileFlowList = fileFlowService.findList(fileNoList, null);
        if (fileFlowList.isEmpty()) {
            log.info("文件上传记录不存在。list.size={}", fileNoList.size());
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "文件上传记录不存在");
        }

        long originalFileSize = 0L;
        for (FileFlowEntity fileFlow : fileFlowList) {
            originalFileSize += fileFlow.getFileSize();
        }

        if (originalFileSize > zip.getMaxOriginalSize()) {
            log.info("单次文件压缩合并，文件总大小超过限制阈值。originalFileSize={} maxOriginalSize={}", originalFileSize,
                    zip.getMaxOriginalSize());
            String message = "单次文件压缩合并，文件总大小不能超过" + (zip.getMaxOriginalSize() / 1024) + "MB";
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), message);
        }

        // 获取文件后缀
        String fileSuffix = zipFileName.substring(zipFileName.lastIndexOf(".") + 1);

        // minio存储的文件名
        String minioFileName = snowflakeService.find() + Constant.SPLIT + zipFileName;

        // 压缩文件对象
        File zipFile = new File(zip.getTempPath() + File.separator + minioFileName);
        if (!zipFile.getParentFile().exists()) {
            boolean bool = zipFile.getParentFile().mkdirs();
            log.info("新建文件目录完成。tempPath={} bool={}", zip.getTempPath(), bool);
        }

        try (FileOutputStream fos = new FileOutputStream(zipFile);
             ZipOutputStream zipOut = new ZipOutputStream(fos);
             WritableByteChannel outputChannel = Channels.newChannel(zipOut)) {

            for (FileFlowEntity fileFlow : fileFlowList) {
                log.info("文件压缩中... fileName={}", fileFlow.getFileName());

                /*
                    获取文件流
                 */
                try (InputStream inputStream = this.getObject(minioProperty.getBucketName(), fileFlow.getFilePath(),
                        fileFlow.getFileName());
                     ReadableByteChannel inputChannel = Channels.newChannel(inputStream)) {

                    // 将文件添加到压缩包
                    zipOut.putNextEntry(new ZipEntry(fileFlow.getFileName()));

                    // 如果输入通道不是FileChannel，使用缓冲区进行读取和写入
                    ByteBuffer buffer = FileUtils.dinamicByteBuffer(fileFlow.getFileSize());

                    // 读取输入通道中的数据
                    while (inputChannel.read(buffer) != -1) {
                        // 切换为读模式
                        buffer.flip();
                        // 将缓冲区中的数据写入输出通道
                        outputChannel.write(buffer);
                        // 清空缓冲区
                        buffer.clear();
                    }

                    // 关闭当前压缩包条目
                    zipOut.closeEntry();
                } catch (IOException e) {
                    log.error("文件写入压缩包异常。{} exception={} message={}", fileFlow, e.getClass().getSimpleName(),
                            e.getMessage(), e);
                    throw new CzzException();
                }
            }
            log.info("所有文件压缩完成");

        } catch (Exception e) {
            log.error("文件压缩异常。exception={} message={}", e.getClass().getSimpleName(), e.getMessage(), e);
            asyncService.execute(() -> {
                // 异步删除临时文件
                boolean bool = zipFile.delete();
                log.info("文件压缩异常，删除临时文件完成。tempPath={} fileName={} bool={}", zip.getTempPath(),
                        zipFile.getName(), bool);
            });
            throw new CzzException();
        }

        try {
            /*
                计算压缩文件的哈希值
             */
            String fileHash = ShaUtils.fileSha256(zipFile);
            log.info("计算得到的压缩文件哈希值为：{}", fileHash);

            // 生成文件路径
            String path = PATH_DATE_FORMAT.format(System.currentTimeMillis());

            // 文件大小
            long size = zipFile.length();

            // 文件类型
            String contentType;
            try {
                contentType = Optional.ofNullable(new Tika().detect(zipFile)).orElse(StringUtils.EMPTY);
            } catch (IOException e) {
                log.error("获取压缩文件类型异常。message={}", e.getMessage(), e);
                throw new CzzException();
            }

            /*
                上传文件
             */
            this.upload(zipFile, minioProperty.getBucketName(), path, minioFileName, contentType);

            /*
                保存文件上传记录
             */
            FileFlowEntity saveFileFlow = new FileFlowEntity();
            saveFileFlow.setFileHash(fileHash)
                    .setFilePath(path)
                    .setFileName(minioFileName)
                    .setOriginalFileName(zipFileName)
                    .setFileSize(size)
                    .setFileType(contentType)
                    .setFileSuffix(fileSuffix)
                    .setBucketName(minioProperty.getBucketName())
                    .setBigFileFlag(CommonEnum.ONE.getValue())
                    .setChunkNum(CommonEnum.MINUS_ONE.getValue())
                    .setChunkSize(CommonEnum.MINUS_ONE.getLongValue());
            fileFlowService.saveOrEdit(saveFileFlow);

            return saveFileFlow.getFileNo();
        } finally {
            asyncService.execute(() -> {
                // 异步删除临时文件
                boolean bool = zipFile.delete();
                log.info("压缩完成，删除临时文件完成。tempPath={} fileName={} bool={}", zip.getTempPath(), zipFile.getName(),
                        bool);
            });
        }

    }

    /**
     * 构造文件下载链接
     * @param fileNo   文件编号
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return 文件下载链接
     */
    public String buildDownloadUrl(Long fileNo, long expire, TimeUnit timeUnit) {
        /*
            获取文件上传记录
         */
        FileFlowEntity fileFlow = this.getFileFlow(fileNo);
        if (Objects.isNull(fileFlow)) {
            log.error("【文件下载】文件上传记录不存在。fileNo={}", fileNo);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "文件不存在");
        }

        // 构造文件下载链接
        return this.buildDownloadUrl(fileFlow, expire, timeUnit);
    }

    /**
     * 构造文件下载链接
     * @param fileHash 文件哈希值
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return 文件下载链接
     */
    public String buildDownloadUrl(String fileHash, long expire, TimeUnit timeUnit) {
        /*
            获取文件上传记录
         */
        FileFlowEntity fileFlow = this.getFileFlow(fileHash);
        if (Objects.isNull(fileFlow)) {
            log.error("【文件下载】文件上传记录不存在。fileHash={}", fileHash);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "文件不存在");
        }

        // 构造文件下载链接
        return this.buildDownloadUrl(fileFlow, expire, timeUnit);
    }

    /**
     * 构造文件下载链接
     * @param fileFlow 文件上传记录
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return 文件下载链接
     */
    public String buildDownloadUrl(FileFlowEntity fileFlow, long expire, TimeUnit timeUnit) {
        // 过期时间（秒）
        int expirySecond = (int) timeUnit.toSeconds(expire);

        // 生成文件下载链接
        return this.getDownloadUrl(
                fileFlow.getBucketName(),
                fileFlow.getFilePath(),
                fileFlow.getFileName(),
                fileFlow.getOriginalFileName(),
                expirySecond
        );
    }

    /**
     * 构造文件下载链接
     * @param fileHashList 文件哈希值列表
     * @return 文件下载链接映射
     */
    public Map<String, String> buildDownloadUrl(List<String> fileHashList, long expire, TimeUnit timeUnit) {
        /*
            获取文件上传记录
         */
        List<FileFlowEntity> fileFlowList = fileFlowService.findList(null, fileHashList);
        if (fileFlowList.isEmpty()) {
                log.error("【批量文件下载】21`126。fileHashList={}", fileHashList);
            return Collections.emptyMap();
        }

        // 过期时间（秒）
        int expirySecond = (int) timeUnit.toSeconds(expire);

        Set<String> fileHashSet = new HashSet<>(fileHashList);
        Map<String, String> fileDownloadUrlMap = new HashMap<>(fileHashList.size());
        for (FileFlowEntity fileFlow : fileFlowList) {
            //
            fileHashSet.remove(fileFlow.getFileHash());

            // 生成文件下载链接
            String fileDownloadUrl = this.getDownloadUrl(
                    fileFlow.getBucketName(),
                    fileFlow.getFilePath(),
                    fileFlow.getFileName(),
                    fileFlow.getOriginalFileName(),
                    expirySecond
            );
            // 映射
            fileDownloadUrlMap.put(fileFlow.getFileHash(), fileDownloadUrl);
        }
        log.info("剩余文件哈希值无文件上传记录：{}", fileHashSet);

        return fileDownloadUrlMap;
    }

    /**
     * 构造文件预览链接
     * @param fileNo   文件编号
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return 文件预览链接
     */
    public String buildPreviewUrl(Long fileNo, long expire, TimeUnit timeUnit) {
        /*
            获取文件上传记录
         */
        FileFlowEntity fileFlow = this.getFileFlow(fileNo);
        if (Objects.isNull(fileFlow)) {
            log.error("【文件预览】文件上传记录不存在。fileNo={}", fileNo);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "文件不存在");
        }

        // 构造文件预览链接
        return this.buildPreviewUrl(fileFlow, expire, timeUnit);
    }

    /**
     * 构造文件预览链接
     * @param fileHash 文件哈希值
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return 文件预览链接
     */
    public String buildPreviewUrl(String fileHash, long expire, TimeUnit timeUnit) {
        /*
            获取文件上传记录
         */
        FileFlowEntity fileFlow = this.getFileFlow(fileHash);
        if (Objects.isNull(fileFlow)) {
            log.error("【文件预览】文件上传记录不存在。fileHash={}", fileHash);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "文件不存在");
        }

        // 构造文件预览链接
        return this.buildPreviewUrl(fileFlow, expire, timeUnit);
    }

    /**
     * 构造文件预览链接
     * @param fileFlow 文件上传记录
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return 文件预览链接
     */
    public String buildPreviewUrl(FileFlowEntity fileFlow, long expire, TimeUnit timeUnit) {
        // 检查文件是否为图片
        if (!this.isImage(fileFlow.getFileType())) {
            log.error("【文件预览】文件不是图片。{}", fileFlow);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(),  fileFlow.getFileName() + "文件不是图片");
        }

        // 过期时间（秒）
        int expirySecond = (int) timeUnit.toSeconds(expire);

        // 生成文件预览链接
        return this.getPreviewUrl(
                fileFlow.getBucketName(),
                fileFlow.getFilePath(),
                fileFlow.getFileName(),
                expirySecond
        );
    }

    /**
     * 构造文件缩略图预览链接
     * @param fileNo   文件编号
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return 缩略图预览链接
     */
    public String buildThumbPreviewUrl(Long fileNo, long expire, TimeUnit timeUnit) {
        /*
            获取文件上传记录
         */
        FileFlowEntity fileFlow = this.getFileFlow(fileNo);
        if (Objects.isNull(fileFlow)) {
            log.error("【缩略图预览】文件上传记录不存在。fileNo={}", fileNo);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "文件不存在");
        }

        // 构造文件缩略图预览链接
        return this.buildThumbPreviewUrl(fileFlow, expire, timeUnit);
    }

    /**
     * 构造文件缩略图预览链接
     * @param fileHash 文件哈希值
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return 缩略图预览链接
     */
    public String buildThumbPreviewUrl(String fileHash, long expire, TimeUnit timeUnit) {
        /*
            获取文件上传记录
         */
        FileFlowEntity fileFlow = this.getFileFlow(fileHash);
        if (Objects.isNull(fileFlow)) {
            log.error("【缩略图预览】文件上传记录不存在。fileHash={}", fileHash);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "文件不存在");
        }

        // 构造文件缩略图预览链接
        return this.buildThumbPreviewUrl(fileFlow, expire, timeUnit);
    }

    /**
     * 构造文件缩略图预览链接
     * @param fileFlow 文件上传记录
     * @param expire   过期时间
     * @param timeUnit 时间单位
     * @return 缩略图预览链接
     */
    public String buildThumbPreviewUrl(FileFlowEntity fileFlow, long expire, TimeUnit timeUnit) {
        if (!this.isImage(fileFlow.getFileType())) {
            log.error("【缩略图预览】文件不是图片。{}", fileFlow);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(),  fileFlow.getOriginalFileName() + "文件不是图片");
        }

        // 过期时间（秒）
        int expirySecond = (int) timeUnit.toSeconds(expire);

        // 生成文件预览链接
        return this.getThumbPreviewUrl(
                fileFlow.getBucketName(),
                fileFlow.getFilePath(),
                fileFlow.getFileName(),
                expirySecond
        );
    }

    /**
     * 构造文件预览链接
     * @param fileHashList 文件哈希值列表
     * @return 文件预览链接映射
     */
    public Map<String, String> buildPreviewUrl(List<String> fileHashList, long expire, TimeUnit timeUnit) {
        /*
            获取文件上传记录
         */
        List<FileFlowEntity> fileFlowList = fileFlowService.findList(null, fileHashList);
        if (fileFlowList.isEmpty()) {
            log.error("【批量文件预览】文件上传记录不存在。fileHashList={}", fileHashList);
            return Collections.emptyMap();
        }

        // 过期时间（秒）
        int expirySecond = (int) timeUnit.toSeconds(expire);

        //
        Set<String> fileHashSet = new HashSet<>(fileHashList);
        Map<String, String> filePreviewUrlMap = new HashMap<>(fileHashList.size());
        for (FileFlowEntity fileFlow : fileFlowList) {
            //
            fileHashSet.remove(fileFlow.getFileHash());
            // 检查文件是否为图片
            if (!this.isImage(fileFlow.getFileType())) {
                log.error("【批量文件预览】文件不是图片。{}", fileFlow);
                throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), fileFlow.getFileHash() + "文件不是图片");
            }
            // 生成文件预览链接
            String filePreviewUrl = this.getPreviewUrl(
                    fileFlow.getBucketName(),
                    fileFlow.getFilePath(),
                    fileFlow.getFileName(),
                    expirySecond
            );
            // 映射
            filePreviewUrlMap.put(fileFlow.getFileHash(), filePreviewUrl);
        }
        log.info("【批量文件预览】剩余文件哈希值无文件上传记录：{}", fileHashSet);

        return filePreviewUrlMap;
    }

    /**
     * 删除文件
     * @param fileNo 文件编号
     */
    public void deleteFile(Long fileNo) {
        /*
            获取文件上传记录
         */
        FileFlowEntity fileFlow = this.getFileFlow(fileNo);
        if (Objects.isNull(fileFlow)) {
            log.error("【文件删除】文件上传记录不存在。fileNo={}", fileNo);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "文件不存在");
        }

        // 删除文件
        this.deleteFile(fileFlow);
    }

    /**
     * 删除文件
     * @param fileHash 文件哈希值
     */
    public void deleteFile(String fileHash) {
        /*
            获取文件上传记录
         */
        FileFlowEntity fileFlow = this.getFileFlow(fileHash);
        if (Objects.isNull(fileFlow)) {
            log.error("【文件删除】文件上传记录不存在。fileHash={}", fileHash);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "文件不存在");
        }

        // 删除文件
        this.deleteFile(fileFlow);
    }

    /**
     * 删除文件
     * @param fileFlow 文件上传记录
     */
    public void deleteFile(FileFlowEntity fileFlow) {
        /*
            删除文件
         */
        this.deleteFile(fileFlow.getBucketName(), fileFlow.getFilePath(), fileFlow.getFileName());

        /*
            删除文件上传记录
         */
        FileFlowEntity editFileFlow = new FileFlowEntity();
        editFileFlow.setId(fileFlow.getId())
                .setDeleteStatus(CommonEnum.ONE.getValue());
        fileFlowService.saveOrEdit(editFileFlow);

        // 删除缓存
        List<String> redisKeyList = new ArrayList<>();
        redisKeyList.add(RedisConstant.STRING + EntityConstant.FLOW_FLOW + RedisConstant.SPLIT + fileFlow.getFileNo());
        redisKeyList.add(RedisConstant.STRING + EntityConstant.FLOW_FLOW + RedisConstant.SPLIT + fileFlow.getFileHash());
        redisHandler.delete(redisKeyList);
    }

    /**
     * 合并切片文件
     * @param fileHash          文件哈希值
     * @param fileName          文件名称
     * @param fileSize          文件大小
     * @param fileType          文件类型
     * @param fileChunkFlowList 切片文件上传记录列表
     * @return 文件编号
     */
    public Long mergeChunkFile(String fileHash, String fileName, Long fileSize, String fileType,
                               List<FileChunkFlowEntity> fileChunkFlowList) {

        String lockKey = RedisConstant.LOCK + CHUNK_FILE + RedisConstant.SPLIT + fileHash;
        RLock lock = redissonClient.getLock(lockKey);
        // 加锁
        lock.lock();
        log.info("合并切片文件，加锁成功");

        try {
            /*
                获取文件上传记录
             */
            FileFlowEntity fileFlow = this.getFileFlow(fileHash);
            if (Objects.nonNull(fileFlow)) {
                log.info("文件已存在，无需重复合并。fileHash={}", fileHash);
                Long fileNo = fileFlow.getFileNo();
                if (!fileName.equals(fileFlow.getOriginalFileName())) {
                    // 文件名称不一致，复制文件上传记录
                    fileNo = fileFlowService.copy(fileFlow.getId(), fileName);
                }
                return fileNo;
            }

            /*
                合并切片文件
             */
            // 生成文件路径
            String path = PATH_DATE_FORMAT.format(System.currentTimeMillis());
            // 切片文件路径列表
            List<String> objectNameList = fileChunkFlowList.stream()
                    .sorted(Comparator.comparingInt(FileChunkFlowEntity::getChunkSerial))
                    .map(fileChunkFlow -> {
                        String chunkFilePath = fileChunkFlow.getFilePath();
                        String chunkFileName = fileChunkFlow.getFileName();
                        return chunkFilePath + "/" + chunkFileName;
                    })
                    .collect(Collectors.toList());

            // 合并文件
            this.mergeFile(minioProperty.getBucketName(), objectNameList, path, fileName, fileType);

            // 获取文件后缀
            String fileSuffix = fileName.substring(fileName.lastIndexOf(".") + 1);

            /*
                保存文件上传记录
             */
            FileFlowEntity saveFileFlow = new FileFlowEntity();
            saveFileFlow.setFileHash(fileHash)
                    .setFilePath(path)
                    .setFileName(fileName)
                    .setOriginalFileName(fileName)
                    .setFileSize(fileSize)
                    .setFileType(fileType)
                    .setFileSuffix(fileSuffix)
                    .setBucketName(minioProperty.getBucketName())
                    .setBigFileFlag(CommonEnum.ZERO.getValue())
                    .setChunkNum(fileChunkFlowList.size())
                    .setChunkSize(fileChunkFlowList.get(0).getFileSize());
            fileFlowService.saveOrEdit(saveFileFlow);

            return saveFileFlow.getFileNo();

        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                // 解锁
                lock.unlock();
            }
        }
    }

    /**
     * 获取文件上传记录
     * @param fileHash 文件哈希值
     * @return 文件上传记录对象
     */
    public FileFlowEntity getFileFlow(String fileHash) {
        String redisKey = RedisConstant.STRING + EntityConstant.FLOW_FLOW + RedisConstant.SPLIT + fileHash;
        /*
            从缓存获取文件上传记录
         */
        FileFlowEntity fileFlow = redisStringHandler.get(redisKey);
        if (Objects.nonNull(fileFlow)) {
            log.info("从缓存获取文件上传记录。fileHash={} fileFlow={}", fileHash, fileFlow);
            // 节流执行
            throttleHandler.throttleRun(() -> {
                // 尝试更新缓存key过期时间
                redisHandler.tryUpdateKeyExpire(redisKey, 150, RandomNumUtils.random5Minutes(10));
            }, redisKey, 1500, TimeUnit.MILLISECONDS);
            return fileFlow;
        }

        String lockKey = RedisConstant.LOCK + redisKey + Constant.SPLIT + RandomNumUtils.getRandomNum(3);
        RLock lock = redissonClient.getLock(lockKey);
        boolean bool = lock.tryLock();
        log.info("获取文件上传记录，尝试加锁完成。lockKey={} bool={}", lockKey, bool);
        if (!bool) {
            lock.lock();
            log.info("获取文件上传记录，加锁成功");
        }
        try {
            /*
                从缓存获取文件上传记录
             */
            if (!bool) {
                fileFlow = redisStringHandler.get(redisKey);
                if (Objects.nonNull(fileFlow)) {
                    log.info("double check 从缓存获取文件上传记录。fileHash={} fileFlow={}", fileHash, fileFlow);
                    return fileFlow;
                }
            }

            /*
                获取文件上传记录
             */
            fileFlow = fileFlowService.find(fileHash);

            if (Objects.nonNull(fileFlow)) {
                /*
                    缓存文件上传记录
                 */
                redisStringHandler.set(redisKey, fileFlow, 5, TimeUnit.MINUTES);

                String _redisKey = RedisConstant.STRING + EntityConstant.FLOW_FLOW + RedisConstant.SPLIT + fileFlow.getFileNo();
                redisStringHandler.set(_redisKey, fileFlow, 5, TimeUnit.MINUTES);
            }

            return fileFlow;
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 获取文件上传记录
     * @param fileNo 文件编号
     * @return 文件上传记录对象
     */
    public FileFlowEntity getFileFlow(Long fileNo) {
        String redisKey = RedisConstant.STRING + EntityConstant.FLOW_FLOW + RedisConstant.SPLIT + fileNo;
        /*
            从缓存获取文件上传记录
         */
        FileFlowEntity fileFlow = redisStringHandler.get(redisKey);
        if (Objects.nonNull(fileFlow)) {
            log.info("从缓存获取文件上传记录。fileNo={} fileFlow={}", fileNo, fileFlow);
            // 节流执行
            throttleHandler.throttleRun(() -> {
                // 尝试更新缓存key过期时间
                redisHandler.tryUpdateKeyExpire(redisKey, 150, RandomNumUtils.random5Minutes(10));
            }, redisKey, 1500, TimeUnit.MILLISECONDS);
            return fileFlow;
        }

        String lockKey = RedisConstant.LOCK + redisKey + Constant.SPLIT + RandomNumUtils.getRandomNum(3);
        RLock lock = redissonClient.getLock(lockKey);
        boolean bool = lock.tryLock();
        log.info("获取文件上传记录，尝试加锁完成。lockKey={} bool={}", lockKey, bool);
        if (!bool) {
            // 加锁
            lock.lock();
            log.info("获取文件上传记录，加锁成功");
        }
        try {
            /*
                从缓存获取文件上传记录
             */
            if (!bool) {
                fileFlow = redisStringHandler.get(redisKey);
                if (Objects.nonNull(fileFlow)) {
                    log.info("double check 从缓存获取文件上传记录。fileNo={} fileFlow={}", fileNo, fileFlow);
                    return fileFlow;
                }
            }

            /*
                获取文件上传记录
             */
            fileFlow = fileFlowService.find(fileNo);

            if (Objects.nonNull(fileFlow)) {
                /*
                    缓存文件上传记录
                 */
                redisStringHandler.set(redisKey, fileFlow, 5, TimeUnit.MINUTES);

                String _redisKey = RedisConstant.STRING + EntityConstant.FLOW_FLOW + RedisConstant.SPLIT + fileFlow.getFileHash();
                redisStringHandler.set(_redisKey, fileFlow, 5, TimeUnit.MINUTES);
            }

            return fileFlow;
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                // 解锁
                lock.unlock();
            }
        }
    }

    /**
     * 判断文件类型不是图片
     * @param fileType 文件类型
     * @return true: 正确 false: 文件类型是图片
     */
    public boolean isImage(String fileType) {
        return StringUtils.startsWith(fileType, "image/");
    }

    /**
     * 上传文件
     * @param file       文件
     * @param bucketName 桶名
     * @param path       路径
     * @param fileName   文件名
     */
    private void upload(MultipartFile file, String bucketName, String path, String fileName) {
        // 创建文件存储桶
        this.createBucket(bucketName);

        try (InputStream inputStream = file.getInputStream()) {

            // 上传文件
            this.upload(inputStream, bucketName, path, fileName, file.getSize(), file.getContentType());

        } catch (IOException e) {
            log.error("文件流操作异常。bucketName={} path={} fileName={} message={}", bucketName, path,
                    fileName, e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 上传文件
     * @param file        文件
     * @param bucketName  桶名
     * @param path        路径
     * @param fileName    文件名
     * @param contentType 文件类型
     */
    private void upload(File file, String bucketName, String path, String fileName, String contentType) {
        // 创建文件存储桶
        this.createBucket(bucketName);

        Path filePath = file.toPath();
        try (InputStream inputStream = Files.newInputStream(filePath)) {

            // 上传文件
            this.upload(inputStream, bucketName, path, fileName, file.length(), contentType);

        } catch (IOException e) {
            log.error("文件流操作异常。bucketName={} path={} fileName={} message={}", bucketName, path,
                    fileName, e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 上传文件
     * @param inputStream 文件输入流
     * @param bucketName  桶名
     * @param path        路径
     * @param fileName    文件名
     * @param fileSize    文件大小
     * @param contentType 文件类型
     * @return 文件上传响应对象
     */
    public ObjectWriteResponse upload(InputStream inputStream, String bucketName, String path, String fileName,
                                      long fileSize, String contentType) {
        // 创建文件存储桶
        this.createBucket(bucketName);

        try {

            // 文件存储名称
            String objectName = path + "/" + fileName;

            // 切片大小
            long partSize = fileSize > 0 ? -1 : 5242880L;

            PutObjectArgs.Builder builder = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .stream(inputStream, fileSize, partSize)
                    .contentType(Optional.ofNullable(contentType).orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE));
            ObjectWriteResponse objectWriteResponse = minioClient.putObject(builder.build());
            log.info("文件上传完成。objectName={} versionId={} etag={}", objectName, objectWriteResponse.versionId(),
                    objectWriteResponse.etag());

            return objectWriteResponse;
        } catch (Exception e) {
            log.error("文件上传异常。bucketName={} path={} fileName={} exception.class={} message={}", bucketName, path,
                    fileName, e.getClass().getSimpleName(), e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 获取文件下载链接
     * @param bucketName       桶名
     * @param path             文件在Minio中的路径
     * @param fileName         文件名
     * @param originalFileName 原始文件名
     * @param expirySecond     链接有效期（秒）
     * @return 文件下载链接
     */
    private String getDownloadUrl(String bucketName, String path, String fileName, String originalFileName, int expirySecond) {
        try {
            // 文件存储名称
            String objectName = path + "/" + fileName;

            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName)
                            .object(objectName)
                            .extraQueryParams(
                                    Collections.singletonMap(
                                            "response-content-disposition",
                                            "attachment; filename=" + originalFileName
                                    )
                            )
                            .expiry(expirySecond)
                            .build()
            );
        } catch (Exception e) {
            log.error("生成文件下载链接异常。bucketName={} path={} fileName={} originalFileName={} expirySecond={} exception.class={} message={}",
                    bucketName, path, fileName, originalFileName, expirySecond, e.getClass().getSimpleName(),
                    e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 获取图片预览链接
     * @param bucketName   桶名
     * @param path         文件在Minio中的路径
     * @param fileName     文件名
     * @param expirySecond 链接有效期（秒）
     * @return 图片预览链接
     */
    private String getPreviewUrl(String bucketName, String path, String fileName, int expirySecond) {
        try {
            // 文件存储名称
            String objectName = path + "/" + fileName;

            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName)
                            .object(objectName)
                            .expiry(expirySecond)
                            .build()
            );
        } catch (Exception e) {
            log.error("生成图片预览链接异常。bucketName={} path={} fileName={} expirySecond={} exception.class={} message={}",
                    bucketName, path, fileName, expirySecond, e.getClass().getSimpleName(), e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 获取图片缩略图预览链接（带质量压缩和尺寸调整）
     * @param bucketName   桶名
     * @param path         文件路径
     * @param fileName     文件名
     * @param expirySecond 链接有效期（秒）
     * @return 缩略图预览链接
     */
    private String getThumbPreviewUrl(String bucketName, String path, String fileName, int expirySecond) {
        try {
            // 文件存储名称
            String objectName = path + "/" + fileName;

            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName)
                            .object(objectName)
                            .extraQueryParams(
                                    Collections.singletonMap(
                                            "response-content-type", "image/jpeg; q=30&width=100&height=100"
                                    )
                            )
                            .expiry(expirySecond)
                            .build()
            );
        } catch (Exception e) {
            log.error("生成缩略图预览链接异常。bucketName={} path={} fileName={} expirySecond={} exception.class={} message={}",
                    bucketName, path, fileName, expirySecond, e.getClass().getSimpleName(), e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 获取文件流
     * @param bucketName 桶名
     * @param path       文件在Minio中的路径
     * @param fileName   文件名
     * @return 文件流
     */
    private InputStream getObject(String bucketName, String path, String fileName) {
        try {
            // 文件存储名称
            String objectName = path + "/" + fileName;

            return minioClient.getObject(
                    GetObjectArgs.builder()
                           .bucket(bucketName)
                           .object(objectName)
                           .build()
            );
        } catch (Exception e) {
            log.error("获取文件流异常。bucketName={} path={} fileName={} exception.class={} message={}", bucketName, path,
                    fileName, e.getClass().getSimpleName(), e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 获取文件元数据
     * @param bucketName 桶名
     * @param path       文件在Minio中的路径
     * @param fileName   文件名
     * @return 文件元数据
     */
    private StatObjectResponse getStat(String bucketName, String path, String fileName) {
        try {
            // 文件存储名称
            String objectName = path + "/" + fileName;

            return minioClient.statObject(
                    StatObjectArgs.builder()
                          .bucket(bucketName)
                          .object(objectName)
                          .build()
            );
        } catch (Exception e) {
            log.error("获取文件元数据异常。bucketName={} path={} fileName={} exception.class={} message={}", bucketName, path,
                    fileName, e.getClass().getSimpleName(), e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 删除文件
     * @param bucketName 桶名
     * @param path       文件在Minio中的路径
     * @param fileName   文件名
     */
    private void deleteFile(String bucketName, String path, String fileName) {
        try {
            // 文件存储名称
            String objectName = path + "/" + fileName;

            minioClient.removeObject(
                    RemoveObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .build()
            );
        } catch (Exception e) {
            log.error("删除文件异常。bucketName={} path={} fileName={} exception.class={} message={}", bucketName, path,
                    fileName, e.getClass().getSimpleName(), e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 合并文件
     * @param bucketName     桶名
     * @param objectNameList 切片文件在Minio中的路径
     * @param path           文件路径
     * @param fileName       文件名
     * @param fileType       文件类型
     */
    private void mergeFile(String bucketName, List<String> objectNameList, String path, String fileName, String fileType) {
        try {
            //
            List<ComposeSource> sources = objectNameList.stream()
                    .map(objectName -> ComposeSource.builder().bucket(bucketName).object(objectName).build())
                    .collect(Collectors.toList());

            // 文件存储名称
            String objectName = path + "/" + fileName;

            // 合并文件
            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .sources(sources)
                            .headers(
                                    Collections.singletonMap(
                                            "Content-Type", fileType
                                    )
                            )
                            .build()
            );

        } catch (Exception e) {
            log.error("合并文件异常。bucketName={} objectNameList={} path={} fileName={} exception.class={} message={}",
                    bucketName, objectNameList, path, fileName, e.getClass().getSimpleName(), e.getMessage(), e);
            throw new CzzException();
        }

        // 异步删除切片文件
        asyncService.execute(() -> {
            log.info("开始异步删除切片文件。objectNameList={}", objectNameList);
            // 删除切片文件
            for (String _objectName : objectNameList) {
                try {
                    minioClient.removeObject(
                            RemoveObjectArgs.builder()
                                    .bucket(bucketName)
                                    .object(_objectName)
                                    .build()
                    );
                } catch (Exception e) {
                    log.error("删除切片文件异常。bucketName={} exception.class={} message={}",
                            bucketName, e.getClass().getSimpleName(), e.getMessage(), e);
                }
            }
            log.info("异步删除切片文件结束");
        });
    }

    /**
     * 创建存储桶
     * @param bucketName 存储桶名称
     */
    private void createBucket(String bucketName) {
        try {
            // 检查存储桶是否已经存在
            String redisKey = RedisConstant.STRING + MINIO + RedisConstant.SPLIT + bucketName;
            if (Boolean.TRUE.equals(redisHandler.hasKey(redisKey))
                    || !minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
                log.info("文件存储桶不存在，新建存储桶：{}", bucketName);
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        } catch (Exception e) {
            log.error("创建存储桶异常。bucketName={} exception.class={} message={}", bucketName,
                    e.getClass().getSimpleName(), e.getMessage(), e);
        }
    }
}
